Skip to content

feat: Governance runtime implementation#899

Open
viswa-uipath wants to merge 9 commits into
mainfrom
feat/governance
Open

feat: Governance runtime implementation#899
viswa-uipath wants to merge 9 commits into
mainfrom
feat/governance

Conversation

@viswa-uipath

@viswa-uipath viswa-uipath commented Jun 9, 2026

Copy link
Copy Markdown

TEST IN PROGRESS: Changes for lang-chain adapter to support governance

Development Package

  • Use uipath pack --nolock to get the latest dev build from this PR (requires version range).
  • Add this package as a dependency in your pyproject.toml:
[project]
dependencies = [
  # Exact version:
  "uipath-langchain==0.11.17.dev1008994879",

  # Any version from PR
  "uipath-langchain>=0.11.17.dev1008990000,<0.11.17.dev1009000000"
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-langchain = { index = "testpypi" }

[tool.uv]
override-dependencies = [
    "uipath-langchain>=0.11.17.dev1008990000,<0.11.17.dev1009000000",
]

Copilot AI review requested due to automatic review settings June 9, 2026 13:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a LangChain/LangGraph governance integration layer to uipath-langchain, wiring UiPath’s governance evaluator into LangChain callbacks and exposing registration via an entry point so uipath-runtime can discover the adapter.

Changes:

  • Introduces a LangChainAdapter + governed proxy wrapper and a callback handler that calls EvaluatorProtocol hooks for model/tool events.
  • Adds governance adapter registration (uipath.governance.adapters entry point) with import-time idempotent self-registration.
  • Updates dependency/lock configuration to pull in updated uipath-core and a dev/test build of uipath-runtime (currently with non-portable lockfile sources).

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 9 comments.

File Description
src/uipath_langchain/governance/adapter.py Implements the LangChain/LangGraph adapter, wrapper, and callback handler that triggers governance evaluations.
src/uipath_langchain/governance/__init__.py Registers the governance adapter on import and exposes an entry-point registration function.
pyproject.toml Adds governance adapter entry point; bumps uipath-core; pins uipath-runtime to a dev build and adds a uv TestPyPI source override.
uv.lock Updates locked dependencies, but currently includes a machine-local Windows editable uipath-runtime source.

Comment on lines +8 to +13
Intercepts:

- ``on_chain_start`` / ``on_chain_end`` → BEFORE_AGENT / AFTER_AGENT
- ``on_llm_start`` / ``on_chat_model_start`` / ``on_llm_end`` → BEFORE_MODEL / AFTER_MODEL
- ``on_tool_start`` / ``on_tool_end`` → TOOL_CALL / AFTER_TOOL

Comment on lines +110 to +117
if hasattr(agent, "callbacks"):
existing = getattr(agent, "callbacks", None) or []
if isinstance(existing, list):
existing.append(callback)
agent.callbacks = existing
else:
agent.callbacks = [callback]
logger.debug("Injected governance callback via agent.callbacks")
Comment on lines +120 to +126
if hasattr(agent, "config"):
config = agent.config or {}
callbacks = config.get("callbacks", [])
callbacks.append(callback)
config["callbacks"] = callbacks
agent.config = config
logger.debug("Injected governance callback via agent.config")
Comment on lines +175 to +182
if config is None:
config = {}
if isinstance(config, dict):
callbacks = config.get("callbacks", [])
if self._callback not in callbacks:
callbacks.append(self._callback)
config["callbacks"] = callbacks
return config
Comment on lines +386 to +395
tool_name = serialized.get("name", "unknown")
tool_args = inputs or {"input": input_str}
self._evaluator.evaluate_tool_call(
tool_name=tool_name,
tool_args=tool_args,
agent_name=self._agent_name,
runtime_id=self._session_id,
trace_id=self._trace_id,
session_state=self._session_state,
)
Comment on lines +401 to +411
def on_tool_end(self, output: Any, **kwargs: Any) -> None:
"""Evaluate AFTER_TOOL rules at tool end."""
try:
tool_result = str(output) if output is not None else ""
self._evaluator.evaluate_after_tool(
tool_name="unknown",
tool_result=tool_result,
agent_name=self._agent_name,
runtime_id=self._session_id,
trace_id=self._trace_id,
)
Comment on lines +83 to +105
def attach(
self,
agent: Any,
agent_id: str,
session_id: str,
evaluator: EvaluatorProtocol,
) -> "GovernedLangChainAgent":
"""Attach governance to a LangChain / LangGraph agent."""
callback = GovernanceCallbackHandler(
evaluator=evaluator,
agent_name=agent_id,
session_id=session_id,
)
self._inject_callback(agent, callback)
return GovernedLangChainAgent(
agent=agent,
adapter=self,
agent_id=agent_id,
session_id=session_id,
evaluator=evaluator,
callback=callback,
)

Comment thread pyproject.toml
Comment on lines 8 to 13
"uipath>=2.10.79, <2.11.0",
"uipath-core>=0.5.17, <0.6.0",
"uipath-core>=0.5.18, <0.6.0",
"uipath-platform>=0.1.61, <0.2.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-runtime==0.11.0.dev1001180441",
"langgraph>=1.1.8, <2.0.0",
"langchain-core>=1.2.11, <2.0.0",
Comment thread pyproject.toml
Comment on lines +161 to +163
[tool.uv.sources]
uipath-runtime = { index = "testpypi" }

viswa-uipath and others added 2 commits June 9, 2026 19:36
Registers a LangChain/LangGraph adapter with the uipath-core adapter
registry so GovernanceRuntime can attach BEFORE_MODEL / AFTER_MODEL /
TOOL_CALL / AFTER_TOOL hooks via LangChain's callback system. Exposed
as a uipath.governance.adapters entry point and self-registers on
import. Bumps uipath-core to 0.6.x and uipath-runtime to 0.11.x to
pick up the new adapter contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viswa-uipath and others added 6 commits June 9, 2026 19:46
uipath-runtime is pinned to a dev pre-release served from testpypi.
uipath==2.10.79 transitively pins uipath-runtime>=0.11.0,<0.12.0,
which excludes pre-releases per PEP 440 (0.11.0.dev* sorts below
0.11.0). uv sync was failing on every CI runner, so no tests ran and
the coverage report rendered as 0%.

Add prerelease = "allow" plus an override-dependencies entry for
uipath-runtime under [tool.uv] so the dev pin can satisfy the
umbrella's stable-only constraint. Re-lock so uipath-runtime resolves
from testpypi instead of the local editable Windows path that wasn't
portable to the Linux/macOS runners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
caplog.at_level(level, logger=NAME) only sets the level on the named
logger; pytest's caplog handler is attached to root, so any message
that exceeds the root logger's effective level still gets filtered out
before reaching the handler. CI's root logger config blocked the
adapter logger's WARNING/DEBUG records — the messages emitted (they
appear in the test output), caplog.records was empty, the
``any(...)`` assertions failed.

Drop the logger= argument so at_level() adjusts the root logger and
caplog reliably captures every record at that severity regardless of
runner config. The tests only assert on the message content, not the
emitting logger, so the change is behaviour-preserving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
caplog wasn't catching adapter logger records on the Linux CI runners
even though the message reached stderr via lastResort. Some interaction
between caplog's root-attached LogCaptureHandler and the package's
propagation chain on those runners — fighting it isn't worth it.

Switch the warning/debug assertions to mock.patch the adapter module's
logger and verify the calls directly. The behaviour under test
(exception swallowed, warning emitted, debug breadcrumb on the
attach path) is the same; the assertion is just more direct and
doesn't depend on logging handler topology.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's mypy config has disallow_any_generics=true and warn_unused_ignores=true,
which the test scaffolding tripped on every inline helper class:

- bare ``list`` / ``dict`` annotations needed parameterisation
- ``# type: ignore[no-untyped-def]`` on the duck-typed agent methods
  weren't actually needed (mypy lets untyped functions slide outside
  the package boundary)

Annotate ``list[Any]`` / ``dict[str, Any]`` and drop the noisy
type-ignore comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…f-blocks cleanly

Two bugs in ``GovernanceCallbackHandler.on_chat_model_start`` /
``on_llm_start`` made the BEFORE_MODEL evaluation noisy and inaccurate
for multi-turn agents:

1. ``str(msg.content)`` produced ``[{'type': 'text', 'text': ...},
   {'type': 'function_call', 'arguments': '{...}'}]`` dict-repr garble
   for list-of-blocks content (multimodal, OpenAI function-call,
   Anthropic tool_use, Claude extended thinking). Regex rules would
   still match by accident, but field-precise rules couldn't navigate
   the shape, quote escapes broke ``\b`` boundary anchors, and the
   meaningful text was buried under dict syntax.

2. Every LLM call concatenated **every** message in the prompt stack.
   For a multi-turn chat the model receives the full history each
   call; the adapter was passing that whole blob to
   ``evaluate_before_model``. A commitment-language violation in
   turn 3's user message kept re-firing on turns 4, 5, 6, ... because
   that text stayed in the prompt for context. Symptom dual of the
   BEFORE_AGENT bug fixed in uipath-runtime: there it under-fired
   (truncation dropped the latest message); here it over-fired
   (history kept re-triggering).

Fix:
- ``on_chat_model_start`` now uses ``_latest_message_input(messages)``
  which takes the last entry of the last batched prompt — the new
  content the LLM is about to respond to. Mirrors the BEFORE_AGENT
  ``latest_only=True`` contract added in uipath-runtime.
- List-of-blocks content is walked via the existing
  ``_extract_block_text`` helper (text + arguments + thinking + input)
  so structured shapes produce clean text, no dict-repr noise.
- ``on_llm_start`` (non-chat completion path) similarly takes only
  ``prompts[-1]`` — batched non-chat calls would otherwise re-scan
  earlier prompts on every callback.
- Both paths cap the extracted blob at ``_BEFORE_MODEL_TEXT_CAP =
  64000`` (matches the runtime side's ``_GOVERNANCE_TEXT_CAP``).

The LLM call is unaffected — ``messages`` / ``prompts`` are read-only
in the callback. Only ``model_input`` (what the evaluator scans)
changes.

Tests:
- Updated ``test_on_llm_start_invokes_evaluator_with_latest_prompt``
  and ``test_on_chat_model_start_latest_message_only`` /
  ``test_on_chat_model_start_dict_messages_latest_only`` to assert
  the new latest-only contract.
- New ``test_on_chat_model_start_list_of_blocks_content`` pins the
  function-call block extraction and the "no dict-repr noise"
  invariant.
- New ``test_on_chat_model_start_caps_model_input`` /
  ``test_on_chat_model_start_empty_messages`` /
  ``test_on_chat_model_start_empty_inner_batch`` cover the cap and
  empty-stack edges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants